/******************************************************************************/
package jamie.org.sil.www.usxUtils;

import java.io.*;


/******************************************************************************/
/**
   A buffered writer class, but one which lets you undo "recent" outputs.
   <p>

   In essence, this merely wraps BufferedWriter and implements all the
   methods provided by that.  The difference is that stuff is written
   initially to a local buffer, which will always retain at least
   n characters (user-settable via {@link #setKeepAtLeast(int) setKeepAtLeast}).
   You can then change your mind about recent writes, and remove them from the
   end of the buffer using {@link #removeTrailing(String) removeTrailingSpaces},
   which deletes one or more instances of a given string at the end of the
   buffer.
   <p>

   You can also check and manipulate the entire local buffer using
   {@link #getBufferContents() getBufferContents} and
   {@link #setBufferContents(String) setBufferContents}.
   <p>

   Note that you should not insert newlines into the text you pass to the
   routines here.  Instead, insert the {@link #C_PseudoLineSeparatorAsString}.
   (This will be converted to newlines at the point that the data is
   actually written out.)  If you need to search for such characters in the
   contents of the buffer, use {@link #C_PseudoLineSeparatorAsPat}.
   <p>

   There are various constructors which you can call as appropriate, depending
   upon the data structures already available.  There is also a special
   constructor -- the one which takes no arguments -- which creates an
   instance which throws away all data passed to it, and which, if queried,
   behaves as though it had received no input whatsoever.
*/

public class BufferedWriterWithUndo
{
  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                                Public                                  **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /**
   * Insert this character into the input text wherever you want a newline --
   * <i>don't</i> insert a newline character directly.
   */
  
  public static final String C_PseudoLineSeparatorAsString =  "\u0008";


  /** Use this as part of any pattern which you use if you need to locate
   *  the pseudo newline character in the buffer.
   */

  public static final String C_PseudoLineSeparatorAsPat    = "\\u0008";


  /****************************************************************************/
  /**
   * Creates an instance associated with an internal buffer.
   *
   * @throws IOException
   */

  public BufferedWriterWithUndo () throws IOException
  {
  }


  /****************************************************************************/
  /**
   * Creates an instance associated with a given OutputStreamWriter.
   *
   * @param w  OutputStreamWriter.
   *
   * @throws IOException
   */

  public BufferedWriterWithUndo (OutputStreamWriter w) throws IOException
  {
    m_Out = new BufferedWriter(w);
  }


  /****************************************************************************/
  /**
   * Closes the stream.
   *
   * @throws IOException
   */

  public void close () throws IOException
  {
    flush();
    if (null != m_Out) m_Out.close();
  }


  /****************************************************************************/
  /**
   * Flushes the output, carrying out any trimming of whitespace if we've
   * been asked to do.
   *
   * @throws IOException
   */

  public void flush () throws IOException
  {
    trimIfNecessary();
    doWrite(m_Buf);
    if (null != m_Out) m_Out.flush();
  }


  /****************************************************************************/
  /**
   * Gets the contents of the buffer.
   *
   * @return Buffer contents.
   */

  public String getBufferContents () throws IOException
  {
    return m_Buf;
  }


  /****************************************************************************/
  /**
   * Gets the last n characters in the buffer.
   *
   * @param n Number of characters required.  (If less than zero, returns
   *          everything which is available.)
   *
   * @return Buffer contents.
   */

  public String getEndOfBufferContents (int n)
  {
    if (n < 0) return m_Buf;
    int start = m_Buf.length() - n;
    if (start < 0) start = 0;
    return m_Buf.substring(start);
  }


  /****************************************************************************/
  /**
   * Outputs a newline.
   *
   * @throws IOException
   */

  public void newLine () throws IOException
  {
    addToBuffer(C_PseudoLineSeparatorAsString);
  }


  /****************************************************************************/
  /**
   * Removes a single instance of the specified string from the buffer.
   *
   * @param s String to trim off.
   *
   * @return True if a trailing instance was found.
   */

  public boolean removeTrailing (String s)
  {
    if (m_Buf.endsWith(s))
    {
       m_Buf = m_Buf.substring(0, m_Buf.length() - s.length());
       return true;
    }
    return false;
  }


  /****************************************************************************/
  /**
   * Removes all trailing instances of a given string from the buffer.
   *
   * @param s String to remove.
   *
   * @return True if anything has been done.
   */

  public boolean removeTrailingAll (String s)
  {
    boolean res = false;
    while (removeTrailing(s))
      res = true;
    return res;
  }


  /****************************************************************************/
  /**
   * Replace all occurrences of a given pattern with another string in whatever
   * buffer we are currently writing to (current destination).
   * 
   * @param pat Pattern.
   * @param replaceWith
   */
  
  public void replaceAll (String pat, String replaceWith)
  {
    m_Buf = m_Buf.replaceAll(pat, replaceWith);
  }
  
  
  /****************************************************************************/
  /**
   * Sets the buffer contents.
   *
   * @param s String to write out.
   */

  public void setBufferContents (String s)
  {
    m_Buf = s;
  }


  /****************************************************************************/
  /**
   * Sets the number of characters to be retained in the buffer at all times.
   * (This is a compromise between reasonable memory-usage, and making sure
   * you have enough data retained to be able to carry out any "undo" operations
   * which may be necessary.)  Bear in mind that it may also have an impact
   * upon efficiency -- strings are immutable, so the bigger the string
   * you retain in memory, the more time will be needed to manipulate it.
   *
   * @param n Number of characters.
   */

  public void setKeepAtLeast (int n)
  {
    m_KeepAtLeast = n;
    if (m_KeepAtMost < n) m_KeepAtMost = n;
  }


  /****************************************************************************/
  /**
   * Indicates whether spaces adjacent to newlines are to be removed.
   *
   * @param doTrim True if they are to be removed.
   */

  public void setTrim_XXX (boolean doTrim) { m_Trim = doTrim; }


  /****************************************************************************/
  /**
   * Writes data to the buffer.
   *
   * @param cbuf  Buffer of data to write out.
   *
   * @throws IOException
   */
  public void write (char[] cbuf) throws IOException
  {
    addToBuffer(new String(cbuf));
  }


  /****************************************************************************/
  /**
   * Writes data to the buffer.
   *
   * @param cbuf Buffer of data to write out.
   * @param off  Offset into buffer at which the required data is found.
   * @param len  Number of characters to write.
   *
   * @throws IOException
   */

  public void write (char[] cbuf, int off, int len) throws IOException
  {
    addToBuffer(new String(cbuf, off, len));
  }


  /****************************************************************************/
  /**
   * Writes data to the buffer.
   *
   * @param c Single character to write out.
   *
   * @throws IOException
   */

  public void write (int c) throws IOException
  {
    addToBuffer(Character.toString((char) c));
  }


  /****************************************************************************/
  /**
   * Writes data to the buffer.
   *
   * @param s String to write out.
   *
   * @throws IOException
   */

  public void write (String s) throws IOException
  {
    addToBuffer(s);
  }


  /****************************************************************************/
  /**
   * Writes data to the buffer.
   *
   * @param s    String to write out.
   * @param off  Offset into string at which the required data is found.
   * @param len  Number of characters to write.
   *
   * @throws IOException
   */

  public void write (String s, int off, int len) throws IOException
  {
    addToBuffer(s.substring(off, off + len));
  }





  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                               Private                                  **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  /**
   * Adds a given string to the buffer, trimming spaces if necessary, and
   * writing out if we've gone over the length we're supposed to retain.  (The
   * routine writes out only the <i>excess</i> data -- we always try to keep
   * enough data in the buffer to be sure we can carry out any necessary
   * manipulations.)
   *
   * @param s String to write out.
   *
   * @throws IOException
   */

  private void addToBuffer (String s) throws IOException
  {
    m_Buf = m_Buf + s;
    trimIfNecessary();
    
    if (null != m_Out)
    {
      if (m_Buf.length() <= m_KeepAtMost) return;
      int n = m_Buf.length() - m_KeepAtLeast;
      doWrite(m_Buf.substring(0, n));
      m_Buf = m_Buf.substring(n);
    }
  }


  /****************************************************************************/
  /**
   * Writes out a single chunk of text, replacing the pseudo line separator with
     a real separator.
   *
   * @param s  String to write out.
   *
   * @throws IOException
   */

  private void doWrite (String s) throws IOException
  {
    if (null != m_Out) m_Out.write(s.replace(C_PseudoLineSeparatorAsString, C_LineSeparator));
  }


  /****************************************************************************/
  /**
   * If we've been asked to trim spaces, remove spaces immediately before and
     after newlines, and replace multiple consecutive spaces by a single space.
   */

  private void trimIfNecessary ()
  {
    if (!m_Trim) return;
    m_Buf = m_Buf.replaceAll("\\s+", " ");
    m_Buf = m_Buf.replace(" " + C_PseudoLineSeparatorAsString, C_PseudoLineSeparatorAsString);
    m_Buf = m_Buf.replaceAll(C_PseudoLineSeparatorAsString + " ", C_PseudoLineSeparatorAsString);
    m_Buf = m_Buf.replaceAll(C_PseudoLineSeparatorAsPat + "+", C_PseudoLineSeparatorAsString);
  }





  /****************************************************************************/
  /****************************************************************************/
  /**                                                                        **/
  /**                               Private                                  **/
  /**                                                                        **/
  /****************************************************************************/
  /****************************************************************************/

  /****************************************************************************/
  private static final String C_LineSeparator = System.getProperty("line.separator");
  private String m_Buf = "";
  private int m_KeepAtLeast = 20;
  private int m_KeepAtMost = 30;
  private BufferedWriter m_Out = null;
  private boolean m_Trim = false;
}

